package request

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"sort"
	"strings"
	"time"
)

const (
	moduleName = "gitee.com/changeden/volcengine-livesaas-request-go"

	contentType          = "application/x-amz-json-1.0"
	defaultAlgorithm     = "AWS4-HMAC-SHA256"
	defaultRequestStruct = "aws4_request"
	defaultCanonicalUri  = "/"

	defaultTimeout = time.Duration(60) * time.Second
)

// 请求协议枚举
const (
	Http = iota
	Https
)

type ProtocolType int32

func (p ProtocolType) String() string {
	switch p {
	case Http:
		return "http://"
	case Https:
		return "https://"
	default:
		return "http://"
	}
}

// 请求方法枚举
const (
	Get  = "GET"
	Post = "POST"
)

var (
	// Client
	invalidHostError            = fmt.Errorf("请传入有效的Client.Host")
	invalidAccessKeyIDError     = fmt.Errorf("请传入有效的Client.AccessKeyID")
	invalidSecretAccessKeyError = fmt.Errorf("请传入有效的Client.SecretAccessKey")
	invalidRegionError          = fmt.Errorf("请传入有效的Client.Region")
	invalidServiceError         = fmt.Errorf("请传入有效的Client.Service")

	// Data
	invalidActionError  = fmt.Errorf("请传入有效的Data.Action")
	invalidVersionError = fmt.Errorf("请传入有效的Data.Version")

	// Request
	invalidResultError = fmt.Errorf("响应结果为空")
)

// 公共参数
type Client struct {
	Host            string
	Protocol        ProtocolType
	AccessKeyID     string
	SecretAccessKey string
	Region          string
	Service         string
	CanonicalUri    string // default: /
	RequestStruct   string // default: aws4_request
	Algorithm       string // default: AWS4-HMAC-SHA256

	Timeout time.Duration
	Logger  *log.Logger

	data Data
}

// 请求参数
type Data struct {
	Action  string
	Version string
	// 私有变量
	body  map[string]interface{}
	query map[string]string
}

func (data *Data) getBody() map[string]interface{} {
	if data.body == nil {
		data.body = make(map[string]interface{})
	}
	return data.body
}
func (data *Data) SetBody(key string, value interface{}) *Data {
	body := data.getBody()
	body[key] = value
	return data
}
func (data *Data) getQuery() map[string]string {
	if data.query == nil {
		data.query = make(map[string]string)
	}
	return data.query
}
func (data *Data) SetQuery(key, value string) *Data {
	query := data.getQuery()
	query[key] = value
	return data
}
func (data *Data) valid() error {
	if data.Action == "" {
		return invalidActionError
	}
	if data.Version == "" {
		return invalidVersionError
	}
	return nil
}

type Response struct {
	ResponseMetadata ResponseMetadata `json:"ResponseMetadata"`
	Result           interface{}      `json:"Result"`
}

func (res *Response) FormatResult(t interface{}) error {
	result := res.Result
	if result == nil {
		return invalidResultError
	}
	jsonStr, err := json.Marshal(result)
	if err != nil {
		return err
	}
	err = json.Unmarshal(jsonStr, &t)
	if err != nil {
		return err
	}
	return nil
}

type ResponseMetadata struct {
	RequestId string         `json:"RequestId"`
	Action    string         `json:"Action"`
	Version   string         `json:"Version"`
	Service   string         `json:"Service"`
	Region    string         `json:"Region"`
	Error     *ResponseError `json:"Error"`
}
type ResponseError struct {
	Code    string `json:"Code"`
	Message string `json:"Message"`
}

func (client *Client) invalid() error {
	if client.Host == "" {
		return invalidHostError
	}
	if client.AccessKeyID == "" {
		return invalidAccessKeyIDError
	}
	if client.SecretAccessKey == "" {
		return invalidSecretAccessKeyError
	}
	if client.Region == "" {
		return invalidRegionError
	}
	if client.Service == "" {
		return invalidServiceError
	}
	return nil
}
func (client *Client) log(label string, info interface{}) {
	if client.Logger != nil {
		client.Logger.Printf("[%s][%s]: %s", moduleName, label, info)
	}
}
func (client *Client) Get(data Data) (*Response, error) {
	return client.req(Get, data)
}
func (client *Client) Post(data Data) (*Response, error) {
	return client.req(Post, data)
}

func (client *Client) req(method string, data Data) (*Response, error) {
	client.data = data
	if err := client.invalid(); err != nil {
		return nil, err
	}
	if err := data.valid(); err != nil {
		return nil, err
	}
	isPostRequest := method == Post
	dateTime, datestamp := genDate()
	host := client.Host
	region := client.Region
	service := client.Service
	canonicalHeaders := fmt.Sprintf("%shost:%s\nx-amz-date:%s\n", conditionStringValue(isPostRequest, fmt.Sprintf("content-type:%s\n", contentType), ""), host, dateTime)
	signedHeaders := fmt.Sprintf("%shost;x-amz-date", conditionStringValue(isPostRequest, "content-type;", ""))
	var body []byte
	if isPostRequest {
		body, _ = json.Marshal(data.getBody())
	}
	requestParameters := conditionStringValue(isPostRequest, string(body), "")
	accessKeyId := client.AccessKeyID
	secretAccessKey := client.SecretAccessKey
	canonicalUri := defaultStringValue(client.CanonicalUri, defaultCanonicalUri)
	algorithm := defaultStringValue(client.Algorithm, defaultAlgorithm)
	requestStruct := defaultStringValue(client.RequestStruct, defaultRequestStruct)
	canonicalQuerystring := genQueryString(data)
	authorizationHeader := signature(requestParameters, method, canonicalUri, canonicalQuerystring, canonicalHeaders, signedHeaders, datestamp, region, service, requestStruct, algorithm, dateTime, accessKeyId, secretAccessKey)
	header := genHeaders(dateTime, authorizationHeader, method)
	bodyReader := genBodyReader(requestParameters, method)
	return client.call(
		fmt.Sprintf("%s%s?%s", client.Protocol.String(), host, canonicalQuerystring),
		method,
		&bodyReader,
		&header,
	)
}
func defaultStringValue(value string, defaultValue string) string {
	return conditionStringValue(value == "", defaultValue, value)
}
func conditionStringValue(condition bool, left, right string) string {
	if condition {
		return left
	}
	return right
}
func genBodyReader(requestParameters string, method string) io.Reader {
	if method == Post {
		return strings.NewReader(requestParameters)
	}
	return nil
}

func genHeaders(dateTime string, authorizationHeader string, method string) http.Header {
	header := http.Header{
		"x-amz-date":    []string{dateTime},
		"Authorization": []string{authorizationHeader},
	}
	if method == Post {
		header.Set("Content-Type", contentType)
	}
	return header
}

func signature(requestParameters string, Method string, canonicalUri string, canonicalQuerystring string, canonicalHeaders string, signedHeaders string, datestamp string, region string, service string, requestStruct string, algorithm string, dateTime string, accessKeyId string, secretAccessKey string) string {
	payloadHash := hash(requestParameters)
	canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", Method, canonicalUri, canonicalQuerystring, canonicalHeaders, signedHeaders, payloadHash)
	credentialScope := fmt.Sprintf("%s/%s/%s/%s", datestamp, region, service, requestStruct)
	stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s", algorithm, dateTime, credentialScope, hash(canonicalRequest))
	signingKey := getSignatureKey(secretAccessKey, datestamp, region, service, requestStruct)
	signatureStr := sign(signingKey, stringToSign)
	authorizationHeader := fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", algorithm, accessKeyId, credentialScope, signedHeaders, hex.EncodeToString(signatureStr))
	return authorizationHeader
}

func genQueryString(data Data) string {
	data.SetQuery("Action", data.Action)
	data.SetQuery("Version", data.Version)
	canonicalQuerystring := ""
	var queryKeys []string
	query := data.getQuery()
	for k := range query {
		queryKeys = append(queryKeys, k)
	}
	sort.Strings(queryKeys)
	for _, k := range queryKeys {
		v := query[k]
		if canonicalQuerystring == "" {
			canonicalQuerystring = fmt.Sprintf("%s=%s", url.QueryEscape(k), url.QueryEscape(v))
		} else {
			canonicalQuerystring = fmt.Sprintf("%s&%s=%s", canonicalQuerystring, url.QueryEscape(k), url.QueryEscape(v))
		}
	}
	return canonicalQuerystring
}

func (client *Client) call(url string, method string, body *io.Reader, header *http.Header) (*Response, error) {
	client.log("请求地址", url)
	client.log("请求方法", method)
	client.log("请求Action", client.data.Action)
	client.log("请求Version", client.data.Version)
	if client.data.body != nil {
		client.log("请求Body", client.data.body)
	}
	if client.data.query != nil {
		client.log("请求Query", client.data.query)
	}
	client.log("请求Header", *header)
	if client.Timeout == 0 {
		client.Timeout = defaultTimeout
	}
	httpClient := http.Client{
		Timeout: client.Timeout,
	}
	req, err := http.NewRequestWithContext(context.Background(), method, url, *body)
	if err != nil {
		client.log("异常信息", err)
		return nil, err
	}
	req.Header = *header
	res, err := httpClient.Do(req)
	if err != nil {
		client.log("异常信息", err)
		return nil, err
	}
	bodyRes := res.Body
	defer func() {
		err := bodyRes.Close()
		if err != nil {
			client.log("异常信息", err)
		}
	}()
	result, err := ioutil.ReadAll(bodyRes)
	if err != nil {
		client.log("异常信息", err)
		return nil, err
	}
	client.log("请求结果", string(result))
	response := Response{}
	err = json.Unmarshal(result, &response)
	if err != nil {
		client.log("异常信息", err)
		return nil, err
	}
	return &response, nil
}

func genDate() (string, string) {
	t := time.Now().UTC()
	dateTime := t.Format("20060102T150405Z")
	date := t.Format("20060102")
	return dateTime, date
}
func getSignatureKey(key, dateStamp, regionName, serviceName, requestStruct string) []byte {
	kDate := sign([]byte("AWS4"+key), dateStamp)
	kRegion := sign(kDate, regionName)
	kService := sign(kRegion, serviceName)
	kSigning := sign(kService, requestStruct)
	return kSigning
}
func sign(key []byte, data string) []byte {
	h := hmac.New(sha256.New, key)
	h.Write([]byte(data))
	return h.Sum(nil)
}
func hash(data string) string {
	d := sha256.New()
	d.Write([]byte(data))
	return hex.EncodeToString(d.Sum(nil))
}
